/**
 * Copyright (C) 2008 Christopher Allen Ogden
 *
 *     This file is part of Odysi.
 *
 *  Odysi is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Odysi is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Odysi.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <SDL.h>
#include <SDL_opengl.h>
#include <stdexcept>
#include <CEGUI.h>
#include <CEGUIDefaultResourceProvider.h>
#include <RendererModules/OpenGLGUIRenderer/openglrenderer.h>
#include "tileManager.h"
#include "gameObject.h"
#include "map.h"
#include "animationListManager.h"
#include "config.h"
#include "log.h"
#include "renderSystem.h"
#include "CEGUISupport.h"
#include "luaState.h"

#define LAYOUT_MAPEDITOR "mapEditor.layout"
#define WINDOW_RED "color/red"
#define WINDOW_GREEN "color/green"
#define WINDOW_BLUE "color/blue"
#define WINDOW_ALPHA "color/alpha"

using std::string;
using std::runtime_error;

Map map(PKGDATADIR FILE_MAP);

int eX, eY;
int mX = 0, mY = 0;
int curTile = 0;
int tileOff = 0;
int objectOff = 0;
bool placeTile = true;
bool snap = true;
int altitude = 0;

#define TILES_PER_WIDTH (SCREEN_WIDTH / TILE_SIZE + 1)
#define TILES_PER_HEIGHT (SCREEN_HEIGHT / TILE_SIZE + 1)
#define OBJECT_PANE_HEIGHT (TILES_PER_HEIGHT * TILE_SIZE - TILE_SIZE * 2 + SCREEN_HEIGHT % TILE_SIZE)

/**
 * Clears the screen before rendering.
 */
void clearScreen(void)
{
    RenderSystem::GS().color(0.0f, 0.0f, 0.0f);
    RenderSystem::GS().solidQuad(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
    RenderSystem::GS().color();
}

/**
 * Draws the map.
 */
void drawMap(void)
{
    map.draw(
            eX - 3,
            eY,
            eX + TILES_PER_WIDTH + 6,
            eY + TILES_PER_HEIGHT + 8,
            -TILE_SIZE * 3,
            0);
}

/**
 * Draws the two tile-selection panes at the top and bottom of the screen.
 */
void drawTileSelectionPanes()
{
    RenderSystem::GS().color(0.0f, 0.0f, 0.0f);
    RenderSystem::GS().solidQuad(0, 0, SCREEN_WIDTH, TILE_SIZE + 2);
    RenderSystem::GS().solidQuad(0, SCREEN_HEIGHT - TILE_SIZE - 2, SCREEN_WIDTH, SCREEN_HEIGHT);
    RenderSystem::GS().color();
    for (unsigned int i = 0; i < TILES_PER_WIDTH - 1; i++) {
        int x = TILE_SIZE * (i % TILES_PER_WIDTH);
        if (i + tileOff < TileManager::GS().getNumTiles(LEVEL_BASE))
            TileManager::GS().drawTile(LEVEL_BASE, i + tileOff, 0, x, 0);
        if (i + objectOff < TileManager::GS().getNumTiles(LEVEL_OBJECTS))
            TileManager::GS().drawTile(LEVEL_OBJECTS, i + objectOff, 0, x, OBJECT_PANE_HEIGHT);
    }
}

/**
 * Draws the selected object.
 */
void drawSelectedObject(void)
{
    int mx = mX / TILE_SIZE * TILE_SIZE;
    int my = mY / TILE_SIZE * TILE_SIZE;
    if (mY >= TILE_SIZE && mY < OBJECT_PANE_HEIGHT) {
        int tileX = mx;
        int tileY = my;
        int placeX = mX - TILE_SIZE / 2;
        int placeY = mY - TILE_SIZE / 2;
        if (placeTile || snap) {
            placeX = tileX;
            placeY = tileY;
        }
        CEGUI::Window *root = CEGUI::System::getSingleton().getGUISheet();
        int red(255), green(255), blue(255), alpha(255);
        try {
            red = getCEGUIInt(WINDOW_RED);
            green = getCEGUIInt(WINDOW_GREEN);
            blue = getCEGUIInt(WINDOW_BLUE);
            alpha = getCEGUIInt(WINDOW_ALPHA);
        } catch (...) {
        }
        RenderSystem::GS().color(8.0f, 0.0f, 0.0f, 0.4f);
        RenderSystem::GS().solidQuad(tileX-1, tileY-1, tileX + TILE_SIZE+1, tileY + TILE_SIZE+1);
        RenderSystem::GS().color(red/255.0f, green/255.0f, blue/255.0f, alpha/255.0f);
        if (placeTile) {
            TileManager::GS().drawTile(LEVEL_BASE, curTile, 0, placeX, placeY);
        } else {
            TileManager::GS().drawTile(LEVEL_OBJECTS, curTile, 0, placeX, placeY - altitude);
        }
        RenderSystem::GS().color();
    }
}

/**
 * Renders a frame.
 */
void render()
{
    clearScreen();
    drawMap();
    drawTileSelectionPanes();
    drawSelectedObject();

    // Render.
    RenderSystem::GS().render();

    // Render interface.
    CEGUI::System::getSingleton().renderGUI();

    // Swap the buffers.
    SDL_GL_SwapBuffers();
}

/**
 * Main entry point for the program.
 *
 * @param argc          The number of arguments passed to the program.
 * @param argv          The arguments passed to the program.
 * @return              0 if all went OK.
 */
int main(int argc, char **argv)
{
    eX = 0;
    eY = 0;

    try {
        Log::GS().info("Odysi map editor is starting...");

        // Initialize SDL.
        Log::GS().info("Initializing SDL...");
        GameObject SDL_INIT(SCREEN_WIDTH, SCREEN_HEIGHT, TITLE);

        // Initialize OpenGL.
        Log::GS().info("Initializing OpenGL...");
        RenderSystem::GS().initSystem();

        // Initialize CEGUI
        Log::GS().info("Initializing CEGUI...");
        initCEGUI(LuaState(luaL_newstate()));
        setCEGUILayout(LAYOUT_MAPEDITOR);
        handleMouseDown(SDL_BUTTON_LEFT);

        // Load the tiles.
        Log::GS().info("Loading tile map...");
        TileManager::GS().load(LEVEL_BASE, PKGDATADIR FILE_TILEMAP_BASE);
        TileManager::GS().load(LEVEL_OBJECTS, PKGDATADIR FILE_TILEMAP_OBJECTS);
        TileManager::GS().load(LEVEL_CHARACTERS, PKGDATADIR FILE_TILEMAP_CHARACTERS);

        // Load the mobiles.
        AnimationListManager::GS().load(PKGDATADIR FILE_CHARACTERS);

        static Uint32 nextTime = 0;

        // Main loop.
        Log::GS().info("Started.");
        bool rightDown = false;
        bool leftDown = false;
        bool run = true;
        double last_time_pulse = 0.001*static_cast<double>(SDL_GetTicks());
        while (run) {
            injectTimePulse(last_time_pulse);

            // Wait a certain amount of time before continuing.
            Uint32 left = nextTime - SDL_GetTicks();
            SDL_Delay(left > TICK_INTERVAL ? 0 : left);
            nextTime = SDL_GetTicks() + TICK_INTERVAL;

            // Tick the map and everything on it.
            map.tick();

            // Determine the next time to wait.
            nextTime = SDL_GetTicks() + TICK_INTERVAL;

            // Handle all the SDL events.
            SDL_Event e;
            while (SDL_PollEvent(&e)) {
                switch (e.type) {
                    case SDL_QUIT:
                        run = false;
                        break;
                    case SDL_MOUSEMOTION:
                        mX = e.button.x;
                        mY = e.button.y;
                        CEGUI::System::getSingleton().injectMousePosition(
                            static_cast<float>(e.motion.x),
                            static_cast<float>(e.motion.y));
                        break;
                    case SDL_MOUSEBUTTONUP:
                        if (!handleMouseUp(e.button.button)) {
                            leftDown = false;
                            rightDown = false;
                        }
                        break;
                    case SDL_MOUSEBUTTONDOWN:
                        if (!handleMouseDown(e.button.button)) {
                            if (e.button.button != SDL_BUTTON_LEFT && e.button.button != SDL_BUTTON_RIGHT) break;

                            leftDown = false;
                            rightDown = false;
                            if (e.button.button == SDL_BUTTON_LEFT) leftDown = true;
                            else rightDown = true;

                            if (mY < TILE_SIZE) {
                                curTile = mX / TILE_SIZE + tileOff;
                                placeTile = true;
                            } else if (mY >= OBJECT_PANE_HEIGHT) {
                                if (mY < OBJECT_PANE_HEIGHT + TILE_SIZE) {
                                    curTile = mX / TILE_SIZE + objectOff;
                                    placeTile = false;
                                }
                            } else {
                                if (!placeTile) {
                                    int x = mX / TILE_SIZE + eX;
                                    int y = mY / TILE_SIZE + eY;
                                    while (x < 0) x += map.getWidth();
                                    while (y < 0) y += map.getHeight();
                                    x = x % map.getWidth();
                                    y = y % map.getHeight();
                                    int red = getCEGUIInt(WINDOW_RED);
                                    int green = getCEGUIInt(WINDOW_GREEN);
                                    int blue = getCEGUIInt(WINDOW_BLUE);
                                    int alpha = getCEGUIInt(WINDOW_ALPHA);
                                    
                                    Object *obj = new Object;
                                    obj->setX(x);
                                    obj->setY(y);
                                    obj->setZ(altitude);
                                    obj->setTileID(curTile);
                                    obj->setID(map.getAvailableID());
                                    obj->setRed(red/255.0f);
                                    obj->setGreen(green/255.0f);
                                    obj->setBlue(blue/255.0f);
                                    obj->setAlpha(alpha/255.0f);
                                    int xOff = mX / TILE_SIZE * TILE_SIZE;
                                    xOff = mX - xOff - TILE_SIZE / 2;
                                    int yOff = mY / TILE_SIZE * TILE_SIZE;
                                    yOff = mY - yOff - TILE_SIZE / 2;
                                    if (!snap) {
                                        obj->setXOffset(xOff, true);
                                        obj->setYOffset(yOff, true);
                                    }
                                    map.addObject(obj, true);
                                }
                            }
                        }
                        break;
                    case SDL_KEYUP:
                        CEGUI::System::getSingleton().injectKeyUp(e.key.keysym.scancode);
                        break;
                    case SDL_KEYDOWN:
                        if (!CEGUI::System::getSingleton().injectKeyDown(SDLKeyToCEGUIKey(e.key.keysym.sym))) {
                            if (e.key.keysym.unicode != 0) {
                                CEGUI::System::getSingleton().injectChar(e.key.keysym.unicode);
                            }
                            switch (e.key.keysym.sym) {
                                case SDLK_d:
                                    map.deleteObjects(mX / TILE_SIZE + eX, mY / TILE_SIZE + eY);
                                    break;
                                case SDLK_p:
                                    snap = !snap;
                                    break;
                                case SDLK_z:
                                    altitude--;
                                    if (altitude < 0) altitude = 0;
                                    break;
                                case SDLK_x:
                                    altitude++;
                                    break;
                                case SDLK_c:
                                    altitude-=16;
                                    altitude-=altitude % 16;
                                    if (altitude < 0) altitude = 0;
                                    break;
                                case SDLK_v:
                                    altitude+=16;
                                    altitude-=altitude % 16;
                                    break;
                                case SDLK_w:
                                    tileOff++;
                                    if (static_cast<unsigned int>(tileOff) + TILES_PER_WIDTH - 1> TileManager::GS().getNumTiles(LEVEL_BASE)) tileOff--;
                                    break;
                                case SDLK_q:
                                    tileOff--;
                                    if (tileOff < 0) tileOff++;
                                    break;
                                case SDLK_s:
                                    objectOff++;
                                    if (static_cast<unsigned int>(objectOff) + TILES_PER_WIDTH - 1> TileManager::GS().getNumTiles(LEVEL_OBJECTS)) objectOff--;
                                    break;
                                case SDLK_a:
                                    objectOff--;
                                    if (objectOff < 0) objectOff++;
                                    break;
                                default:
                                    break;
                            }
                        }
                        break;
                }
            }

            if (leftDown && placeTile && mY > TILE_SIZE && mY < OBJECT_PANE_HEIGHT) {
                map.setID(mX / TILE_SIZE + eX, mY / TILE_SIZE + eY, 0, curTile);
            }
            if (rightDown && placeTile && mY > TILE_SIZE && mY < OBJECT_PANE_HEIGHT) {
                map.setID(mX / TILE_SIZE + eX, mY / TILE_SIZE + eY, 1, curTile);
            }

            // Render the screen.
            render();

            // Handle movement.
            Uint8 *keystate = SDL_GetKeyState(NULL);
            if (keystate[SDLK_LEFT]) {
                eX--;
                if (eX < 0) eX = map.getWidth() - 1;
            } else if (keystate[SDLK_RIGHT]) {
                eX++;
                if (static_cast<unsigned int>(eX) >= map.getWidth()) eX = 0;
            } else if (keystate[SDLK_UP]) {
                eY--;
                if (eY < 0) eY = map.getHeight() - 1;
            } else if (keystate[SDLK_DOWN]) {
                eY++;
                if (static_cast<unsigned int>(eY) >= map.getHeight()) eY = 0;
            }
        }
        Log::GS().info("Shutting down...");
    } catch(std::exception &e) {
        Log::GS().error(e.what());
    } catch(...) {
        Log::GS().error("Unknown exception.");
    }

    return 0;
}
